package com.android.pa;

import java.util.Calendar;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import android.content.Context;
import android.util.Log;

/**
 * So basically this guy needs to have the algorithm implemented and then we need to
 * think how he reacts to each different different kind of call, and to each life cycle event.
 */
public class Scheduler
{

	Context 						_con;
	private PACalendar 				_PACalendar;
	private NotificationManager 	_NotificationManager;

	/* IMPLEMENTATION */

	/**
	 * Constructor.
	 * @param c the application context.
	 */
	public Scheduler(Context c)
	{
		_con = c;
		_PACalendar = new PACalendar(_con);
		_NotificationManager = new NotificationManager(_con);
	}

	/**
	 * Schedules a new task.
	 * @param newTask the task to schedule.
	 * @return a list of updated tasks (for debug).
	 */
	public List<Task> schedule(Task newTask)
	{
		Log.d("sched", "entered schedule");
		if (newTask == null)
		{
			Log.d("sched", "Cannot schedule null.");
			return null;
		}

		LinkedList<Task> changed = new LinkedList<Task>();

		// There could be several cases:
		// 1. Task is fixed but its time is taken by a fixed task --> schedule.
		// 2. Task is fixed but its time is taken by a fluid task --> swap and reschedule fluid task, if possible.
		// 3. Task is fluid --> schedule fluid task, if possible.

		// Handling 1 and 2 (fixed task).
		if (!newTask.isFluid())
		{
			boolean isFree = _PACalendar.isFreeTime(newTask.getStartTime(), newTask.getEndTime());
			// If is free, add event to list, mark only it as changed and return.
			if (isFree)
			{
				changed.add(newTask);
				commit(changed);
				return changed;
			}

			// Find overlapping task.
			LinkedList<Task> overlapping = _PACalendar.getSchedule(newTask.getStartTime(), newTask.getEndTime());

			assert(overlapping != null && overlapping.size() > 0);
			
			Task overlappingTask = overlapping.get(0);

			// Case 1 - fixed on fixed --> schedule anyway.
			if (!overlappingTask.isFluid())
			{
				changed.add(newTask);
				commit(changed);
				return changed;
			}

			// Case 2. We check if the fluid task can be handled.
			// WE DO NOT HANDLE CASES WHERE MORE THAN ONE TASK IS OVERLAPPING.
			Calendar when = findSlot((FluidTask) overlappingTask, null);

			if (when == null)
				return null; // No slot found.

			// Found slot, schedule fixed task in place of fluid and reschedule fluid task.
			changed.add(newTask);

			addStartEndToFluid((FluidTask) overlappingTask, when);
			changed.add(overlappingTask);

			commit(changed);
			return changed;
		} // Handles fixed task.

		// Case 3 - schedule fluid task.
		FluidTask newnewTask = (FluidTask) newTask;

		// Case 3. Find a slot for fluid task.
		Calendar when = findSlot(newnewTask, null);

		if (when == null)
		{
			// No slot found, try to move other fluid task.
			changed = secondStage(newnewTask);
			commit(changed);
			return changed;
		}

		// Found slot for fluid task.
		addStartEndToFluid(newnewTask, when);
		changed.add(newnewTask);
		commit(changed);
		return changed;
	}

	/**
	 * Extend task.
	 * @param task the task to extend.
	 * @param timeToAdd the time to add.
	 */
	public String addTimeToTask(FluidTask task, Long timeToAdd)
	{
		Log.d("sched", "Add time to task");
		
		return null;
//
//		assert(task != null);
//
//		// Check that we do not pass deadline.
//		if (task.getEndTime().getTimeInMillis() + timeToAdd >= task.getDeadline().getTimeInMillis())
//			return "Cannot add time past deadline.";
//
//		LinkedList<Task> changed = new LinkedList<Task>();
//
//		// Roll for checking free time.
//		Calendar intervalStart = Calendar.getInstance();
//		intervalStart.setTimeInMillis(task.getEndTime().getTimeInMillis() + 100);
//
//		Calendar intervalEnd = Calendar.getInstance();
//		intervalEnd.setTimeInMillis(task.getEndTime().getTimeInMillis() + timeToAdd);
//
//		// If there is time to extend the task.
//		if (_PACalendar.isFreeTime(intervalStart, intervalEnd))
//		{
//			task.getEndTime().setTimeInMillis(task.getEndTime().getTimeInMillis() + timeToAdd);
//			changed.add(task);
//			commit(changed);
//			return null;
//		}
//
//		// Time is not free.
//		Task next = _PACalendar.getNextTask(task);
//		
//		assert(next != null);
//
//		if (!next.isFluid())
//			return "Cannot move fixed task.";
//
//		Calendar from = Calendar.getInstance();
//		from.setTimeInMillis(task.getEndTime().getTimeInMillis() + timeToAdd + 100);
//
//		Calendar when = findSlot((FluidTask) next, from);
//
//		if (when == null)
//			return "Failed rescheduling.";
//
//		// Reschedule.
//		addStartEndToFluid((FluidTask) next, when);
//		changed.add(next);
//		task.getEndTime().setTimeInMillis(task.getEndTime().getTimeInMillis() + timeToAdd);
//		changed.add(task);
//
//		commit(changed);
//		return null;
	}
	
	/**
	 * Write changes to calendar and notification manager.
	 * @param tasks the tasks to commit.
	 */
	private void commit(LinkedList<Task> tasks)
	{	
		if (tasks == null || tasks.isEmpty())
		{
			Log.d("sched", "commit: null or empty task list.");
			return;
		}

		_PACalendar.updateSchedule(tasks);
		_NotificationManager.setAlarms(tasks);
		
		for (Task t : tasks)
			Log.d("sched", "Adding task:\n" + t.toString());
	}

	/**
	 * We need to postpone this task by 15 minutes.
	 * @param task the task to postpone.
	 * @return String representation of result.
	 */
	public String snooze(FluidTask task)
	{
		Log.d("sched", "snooze");
		
		return null;
//
//		assert(task != null);
//
//		LinkedList<Task> changed = new LinkedList<Task>();
//
//		// Roll start and end.
//		task.getStartTime().roll(Calendar.MINUTE, 15);
//		task.getEndTime().roll(Calendar.MINUTE, 15);
//
//		// Roll for checking free time.
//		Calendar intervalStart = Calendar.getInstance();
//		intervalStart.setTimeInMillis(task.getEndTime().getTimeInMillis() + 100);
//
//		Calendar intervalEnd = Calendar.getInstance();
//		intervalEnd.setTimeInMillis(task.getEndTime().getTimeInMillis() + 60000 * 15);
//
//		// If there is time to postpone the task
//		if (_PACalendar.isFreeTime(intervalStart, intervalEnd))
//		{
//			Log.d("sched", "in snooze: is free time true");
//			changed.add(task);
//			commit(changed);
//			return null;
//		}
//		
//		Log.d("sched", "in snooze: is free time false");
//
//		// Time is not free.
//		Task next = _PACalendar.getNextTask(task);
//		
//		assert(next != null);
//
//		if (!next.isFluid())
//			return "Cannot move fixed task.";
//
//		// Other is fluid, try to reschedule it.
//		Calendar when = findSlot((FluidTask) next, intervalEnd);
//		if (when == null)
//			return "No time found for other fluid task.";
//
//		// Found new time, need to reschedule both.
//		changed.add(task);
//
//		addStartEndToFluid((FluidTask) next, when);
//		changed.add(next);
//		commit(changed);
//		return null;
	}

	/**
	 * Add a start and end time to a fluid task.
	 * @param task the task to add to.
	 * @param start the start time.
	 */
	private void addStartEndToFluid(FluidTask task, Calendar start)
	{
		task.setStart(start);
		Calendar end = Calendar.getInstance();
		end.setTimeInMillis(start.getTimeInMillis() + task.getDuration());
		task.setEnd(end);
	}

	/**
	 * Get the space before and after the task.
	 * @param before the previous task.
	 * @param task the task.
	 * @param after the following task.
	 * @return the time around the task, inclusive.
	 */
	private long getSpaceAroundTask(Task before, Task task, Task after)
	{    	
		// Actual task duration.
		long duration = task.getEndTime().getTimeInMillis() - task.getStartTime().getTimeInMillis();

		// Before space.
		if (before != null)
			duration += task.getStartTime().getTimeInMillis() - before.getEndTime().getTimeInMillis();

		// After space.
		if (after != null)
			duration += after.getStartTime().getTimeInMillis() - task.getEndTime().getTimeInMillis();

		return duration;
	}

	/**
	 * Add start and end padding to a task list.
	 * @param task the task to pad.
	 * @param tasks the task list containing the task.
	 */
	private void addPadding(FluidTask task, List<Task> tasks)
	{
		Log.d("sched", "adding padding");
		// If there is no task right now, add a virtual task from now of length 5 minutes.
		if ( !(tasks.get(0).getStartTime().getTimeInMillis() <= System.currentTimeMillis() &&
				tasks.get(0).getEndTime().getTimeInMillis() >= System.currentTimeMillis()) )
		{
			Calendar in5Minutes = Calendar.getInstance();
			in5Minutes.roll(Calendar.MINUTE, 5);
			tasks.add(0, new FixedTask("dummyFindSlot", Calendar.getInstance(), in5Minutes, "here and there"));
		}

		// If there is no task at the deadline of the task to be scheduled, add a virtual task at the end.
		if ( !(tasks.get(tasks.size() - 1).getStartTime().getTimeInMillis() <= task.getDeadline().getTimeInMillis() &&
				tasks.get(tasks.size() - 1).getEndTime().getTimeInMillis() >= task.getDeadline().getTimeInMillis()) )
		{
			Calendar last5Minutes = Calendar.getInstance();
			last5Minutes.setTimeInMillis(task.getDeadline().getTimeInMillis() - 60000);
			tasks.add(tasks.size(), new FixedTask("dummyFindSlot", last5Minutes, task.getDeadline(), "here and there"));
		}    	
	}

	/**
	 * Tries to move fluid tasks which are scheduled until this fluid task's duration.
	 * @param task the task for second stage.
	 * @return list of changed tasks, or null if failed.
	 */
	private LinkedList<Task> secondStage(FluidTask task)
	{
		Log.d("sched", "entered second stage");

		LinkedList<Task> tasks = _PACalendar.getSchedule(Calendar.getInstance(), task.getDeadline());
		LinkedList<Task> changed = new LinkedList<Task>();

		addPadding(task, tasks);

		for (Task current2 : tasks)
		{
			if (!current2.isFluid())
				continue;

			FluidTask current = (FluidTask) current2;

			int index = tasks.indexOf(current);
			Task before = null;
			if (index >= 1)
				before = tasks.get(index - 1);
			Task after = null;
			if (index < tasks.size())
				after = tasks.get(index + 1);

			// Check if moving the current task would help.
			if (getSpaceAroundTask(before, current, after) < task.getDuration())
				continue;

			Calendar startAt = null;
			if (before != null && before.getEndTime() != null)
			{
				startAt = before.getEndTime();
				startAt.setTimeInMillis(startAt.getTimeInMillis() + task.getDuration());
			}

			Calendar when = findSlot(current, startAt);

			if (when == null) // No time found.
				continue;

			// Found new time for current. Need to reschedule current and schedule task.
			addStartEndToFluid(task, before.getEndTime());
			changed.add(task);

			addStartEndToFluid(current, when);
			changed.add(current);

			return changed;
		}

		return null;
	}

	/**
	 * Finds a slot for event from the start time of the first event in the list to the end
	 * time of the last event in the list. If a time is found, it updates the list of events with
	 * the schedules event.
	 * @param event the event to schedule.
	 * @return the time to schedule the task, or null if not found.
	 */
	private Calendar findSlot(FluidTask task, Calendar from)
	{
		Log.d("sched", "findSlot");
		if (from == null)
			from = Calendar.getInstance();

		// Get task list from NOW until fluid task's deadline.
		Log.d("sched", "getting tasks from PACal");
		List<Task> tasks = _PACalendar.getSchedule(from, task.getDeadline());

		// No obstructions --> just schedule.
		if (tasks == null || tasks.isEmpty())
		{
			from.roll(Calendar.MINUTE, 1);
			return from;
		}

		Log.d("sched", "got " + tasks.size() + " tasks");

		addPadding(task, tasks);

		Task first = null, second = null;
		long slotSize = 0L;
		Iterator<Task> it = tasks.iterator();

		first = it.next();

		Log.d("sched", "iterating over tasks");
		// Iterate over tasks and search for a hole.
		while (it.hasNext())
		{
			Log.d("sched", "while loop");

			second = it.next();
			slotSize = second.getStartTime().getTimeInMillis() - first.getEndTime().getTimeInMillis();
			if (slotSize >= task.getDuration())
				return first.getEndTime();

			first = second;
		}

		return null;
	}

}